// Copyright 2010, Vanya Davidenko.
// Используемая кодировка: UTF-8.

#ifndef SCRW_HTML_URL_EXTRACTOR_H_
#define SCRW_HTML_URL_EXTRACTOR_H_

// Данный хедер включает также поддержку TryCriticalSection.
// Он должен идти самым первым, чтобы windows.h была подключена с заданными
// настройками (через макросы), а не проигнорирована (из-за header guard).
#ifdef _WIN32
# include <libv/windows_lean.h>
#endif  // _WIN32

#include <string>
#include <deque>

#include <unicode/unistr.h>
#include <unicode/regex.h>

#include <libv/check.h>

#include "curl.h"
#include "common.h"


namespace scrw
{

class HtmlUrlExtractor
{
  public:
    // У ICU, похоже, не активируется режим регулярных выражений с комментариями
    // через ?x:, а через явный флаг в конструкторе RegexMatcher все нормально.
    HtmlUrlExtractor(const bool grab_nofollow)
        : err_(U_ZERO_ERROR),

          grab_nofollow_(grab_nofollow),

          // Некорректная обработка: <a href="</a>b">
          // Вообще, <base> должен быть внутри <head>, однако, не все это
          // соблюдают, а браузеры обрабатывают любые варианты.
          // Используется оптимизация (если поддерживается движком) поиска
          // литерала: сначала за линейное время простым поиском будет найдено
          // '<', а затем уже начнется обработка КА.
          // Оба типа тегов выделяются за один проход текста.
          a_or_base_matcher_(
              "(?i:                    "
              "    <(?:                "
              "        (a .+? </a>)    "
              "        |               "
              "        (base .+? /?>)  "  // В XHTML обязательно <base ... />
              "    )                   "  // В HTML можно <base ... >
              ")                       ",
              UREGEX_COMMENTS | UREGEX_DOTALL, err_),

          // На основе rfc3986.
          // В документации не показано, как заэкранировать символ '#' в режиме
          // UREGEX_COMMENTS, воспринимаемый как начало комментария.
          //
          // На данный момент поддерживается только https?, так как по его
          // заголовку можно определить mime-тип или хотя бы размер, что бы не
          // нарваться, например, на HD-фильм.
          domain_matcher_("^https?://[^/?#]++", 0, err_),

          protocol_matcher_("^https?:", 0, err_),

          // Работает за линейное время.
          href_matcher_(
              "(?i:                       "
              "    href \\s*+ = \\s*+     "
              "        (?:                "  // ссылка может быть
              "            '([^']++)'     "  // в апострофах (стандарт)
              "            |              "
              "            \"([^\"]++)\"  "  // в кавычках (стандарт)
              "            |              "
              "            ([^\"'>\\s]++) "  // без всего, до конца тега
              "        )                  "  // или разделителя
              ")                          ",
              UREGEX_COMMENTS | UREGEX_DOTALL, err_),

          nofollow_matcher_(
              "(?i:                   "
              "    rel \\s*+ = \\s*+  "
              "    (?:                "
              "        '[^']*?        "  // скорее всего в rel одно значение
              "        |              "  // (хотя может быть несколько через
              "        \"[^\"]*?      "  // запятую), поэтому ленивый поиск
              "        |              "  // будет быстрее.
              "        [^\"'>]*?      "
              "    )                  "
              "    \\b nofollow \\b   "  // \\b — граница слова.
              ")                      ",
              UREGEX_COMMENTS | UREGEX_DOTALL, err_),

          // Работает за линейное время.
          // Находит самый первый якорь, даже если их несколько (по ошибке).
          anchor_matcher_("#.*", 0, err_),

          exclude_protocol_matcher_(
                          "(?i:^(?:javascript|mailto|skype|xmpp|ed2k|news| "
                          "        gopher|data|man|smb|nfs|network):)      ",
                          UREGEX_COMMENTS, err_)
    {
        VERIFY(U_SUCCESS(err_) == TRUE);
    }


    HtmlUrlExtractor(const HtmlUrlExtractor& rhv)
        : err_(U_ZERO_ERROR),
          grab_nofollow_(rhv.grab_nofollow_),
          a_or_base_matcher_(
              rhv.a_or_base_matcher_.pattern().pattern(),
              rhv.a_or_base_matcher_.pattern().flags(),
              err_),
          domain_matcher_(
              rhv.domain_matcher_.pattern().pattern(),
              rhv.domain_matcher_.pattern().flags(),
              err_),
          protocol_matcher_(
              rhv.protocol_matcher_.pattern().pattern(),
              rhv.protocol_matcher_.pattern().flags(),
              err_),
          href_matcher_(
              rhv.href_matcher_.pattern().pattern(),
              rhv.href_matcher_.pattern().flags(),
              err_),
          nofollow_matcher_(
              rhv.nofollow_matcher_.pattern().pattern(),
              rhv.nofollow_matcher_.pattern().flags(),
              err_),
          anchor_matcher_(
              rhv.anchor_matcher_.pattern().pattern(),
              rhv.anchor_matcher_.pattern().flags(),
              err_),
          exclude_protocol_matcher_(
              rhv.exclude_protocol_matcher_.pattern().pattern(),
              rhv.exclude_protocol_matcher_.pattern().flags(),
              err_)
    {
        VERIFY(U_SUCCESS(err_) == TRUE);
    }


    ::std::deque<UAsciiUrl> Extract(const UAsciiUrl& url, const Html& html)
    {
        PRECOND(url.isBogus() == FALSE);
        PRECOND(html.isBogus() == FALSE);


        // Сначала получаем все page_url и самый последний base,
        // который может быть даже внизу страницы, хотя это и не по стандарту.
        UAsciiUrl base_path;
        ::std::deque<Html> found_a_tags;
        GetTagsABase(html, &base_path, &found_a_tags);
        if ( base_path.isEmpty() )
            base_path = url;

        const UAsciiUrl base_folder = ExtractBaseFolderFromUrl(base_path);
        const UAsciiUrl domain = ExtractDomainFromUrl(base_path);
        const UAsciiUrl protocol = ExtractProtocolFromUrl(base_path);


        ::std::deque<UAsciiUrl> result;
        for ( ::std::deque<Html>::const_iterator \
              i = found_a_tags.begin() ; i != found_a_tags.end() ; ++i )
        {
            if ( !grab_nofollow_ && NoFollow(*i) ) continue;

            UAsciiUrl href = GetHref(*i);

            if ( ContainsExcludeProtocol(href) ) continue;

            RemoveHTMLAnchor(&href);
            if ( href.isEmpty() ) continue;

            if ( IsAbsolute(href) )
            {
                result.push_back(href);
            }
            else
            {
                // Результаты получены исходя из
                // http://www.webreference.com/html/tutorial2/3.html
                // и тестов в браузере.
                UAsciiUrl the_url;
                if ( href[0] == '/' )
                {
                    if ( href[1] == '/' )
                        the_url = protocol + href;
                    else
                        the_url = ConcatBaseAndPath(domain, href);
                }
                else if ( href[0] == '?' )  // адреса с # уже отфильтрованы
                {
                    the_url = base_path + href;
                }
                else
                {
                    the_url = ConcatBaseAndPath(base_folder, href);
                }
                result.push_back(the_url);
            }
        }

        return result;
    }



  private:
    HtmlUrlExtractor& operator=(HtmlUrlExtractor&);

    /** Получение значения атрибута href из содержимого тега. */
    UAsciiUrl GetHref(const Html& a_or_base_tag)
    {
        href_matcher_.reset(a_or_base_tag);

        if ( href_matcher_.find() == FALSE )
            return UAsciiUrl();

        const UAsciiUrl url = href_matcher_.group(1, err_) +
                              href_matcher_.group(2, err_) +
                              href_matcher_.group(3, err_);

        // На всякий случай проверяем ошибку, которой не может быть, так как
        // find точно сработал, а групп точно 3 штуки.
        VERIFY(U_SUCCESS(err_) == TRUE);

        return url;
    }


    /** Выделение содержимого открывающих тегов a и base из переданного html. */
    void GetTagsABase(const Html& html,
                      UAsciiUrl* base,
                      ::std::deque<Html>* tags)
    {
        a_or_base_matcher_.reset(html);
        while ( a_or_base_matcher_.find() )
        {
            assert(a_or_base_matcher_.group(1, err_).isEmpty() +
                   a_or_base_matcher_.group(2, err_).isEmpty() == 1);

            const Html a_tag = a_or_base_matcher_.group(1, err_);
            VERIFY(U_SUCCESS(err_) == TRUE);

            if ( a_tag.isEmpty() == TRUE )
            {
                const Html base_tag = a_or_base_matcher_.group(2, err_);
                VERIFY(U_SUCCESS(err_) == TRUE);

                const UnicodeUrl base_href = GetHref(base_tag);

                // Base может быть без href — это не единственный атрибут.
                if ( base_href.isEmpty() )
                    continue;

                // Браузеры нормально обрабатывают несколько base, учитывая
                // последний, значит, кто-то может это использовать.
                *base = base_href;
            }
            else
            {
                tags->push_back(a_tag);
            }
        }
    }


    /** Выделение адреса домена из url. */
    UAsciiUrl ExtractDomainFromUrl(const UAsciiUrl& url)
    {
        if ( domain_matcher_.reset(url).find() == FALSE )
            return UAsciiUrl();

        const UAsciiUrl domain = domain_matcher_.group(0, err_);
        VERIFY(U_SUCCESS(err_) == TRUE);
        return domain;
    }


    /** Выделение адреса домена из url. */
    UAsciiUrl ExtractProtocolFromUrl(const UAsciiUrl& url)
    {
        if ( protocol_matcher_.reset(url).find() == FALSE )
            return UAsciiUrl();

        const UAsciiUrl protocol = protocol_matcher_.group(0, err_);
        VERIFY(U_SUCCESS(err_) == TRUE);
        return protocol;
    }



    // http://www.webreference.com/html/tutorial2/3.html
    UAsciiUrl ExtractBaseFolderFromUrl(const UAsciiUrl& url)
    {
        const int32_t i = url.lastIndexOf('/');
        if ( i < 7 ) return ExtractDomainFromUrl(url);

        UAsciiUrl result;
        url.extractBetween(0, i, result);
        return result;
    }


    /** Проверка наличия атрибута rel="nofollow" в содержимом тега a. */
    bool NoFollow(const Html& a_tag)
    {
        return nofollow_matcher_.reset(a_tag).find() == TRUE;
    }


    bool ContainsExcludeProtocol(const UAsciiUrl& url)
    {
        exclude_protocol_matcher_.reset(url);
        return exclude_protocol_matcher_.find() == TRUE;
    }



    /** Удаление якорных ссылок из адреса.
      * Пример: http://example.org/foo#bar -> http://example.org/foo. */
    void RemoveHTMLAnchor(UAsciiUrl* url)
    {
        *url = anchor_matcher_.reset(*url).replaceAll(UAsciiUrl(), err_);
        VERIFY(U_SUCCESS(err_) == TRUE);
    }


    /** Проверка условия, что адрес url — абсолютный, т.е. содержит домен. */
    bool IsAbsolute(const UAsciiUrl& url)
    {
        return ExtractDomainFromUrl(url).isEmpty() == FALSE;
    }


    /** Объединение базового адреса (домена) и пути. */
    UAsciiUrl ConcatBaseAndPath(const UAsciiUrl& base,
                                const UAsciiUrl& path)
    {
        // operator[] возвращает 0xffff при выходе за границу массива.
        UAsciiUrl result;
        if ( base[base.length() - 1] == '/' )
            result.append(base, 0, base.length() - 1);
        else
            result.append(base, 0, base.length());

        result.append('/');

        if ( path[0] == '/' )
            result.append(path, 1, path.length());
        else
            result.append(path, 0, path.length());

        return result;
    }



    /** Переменная, используемая в качестве кода ошибок для всех вызовов методов
      * ICU. Использование общей переменной согласуется с политикой ICU:
      * в случае, если предыдущий вызов записал в err_ код ошибки, следующий
      * ничего не будет выполнять. */
    UErrorCode err_;

    /** Флаг, определяющий игнорируется ли атрибут rel="nofollow".
      * Если true, то метод Grab будет возвращать и такие ссылки. */
    bool grab_nofollow_;

    /** Выделение из html-файла содержимого открывающих тегов a и base.*/
    icu::RegexMatcher a_or_base_matcher_;

    /** Выделение из URL домена .
      * Пример: http://example.com/123 -> http://example.com. */
    icu::RegexMatcher domain_matcher_;

    /** Выделение из URL протокола домена .
      * Пример: http://example.com/123 -> http:/. */
    icu::RegexMatcher protocol_matcher_;

    /** Выделение значения атрибута href. */
    icu::RegexMatcher href_matcher_;

    /** Поиск атрибута rel="nofollow" в теле тега. */
    icu::RegexMatcher nofollow_matcher_;

    /** Поиск первой якорной ссылки. http://example.com/foo#bar -> #bar. */
    icu::RegexMatcher anchor_matcher_;

    /** Поиск различных протоколов (например, mailto:) в начале адреса.
      * Данное регулярное выражение позволяет отличить абсолютный адрес с
      * протоколом, разделяемым двоеточием (например, mailto:
      * или skype:) от относительного адреса с двоеточием (например,
      * hello:world), кроме как по таблице протоколов. */
    icu::RegexMatcher exclude_protocol_matcher_;
};

}

#endif  // SCRW_URL_GRABBER_H_
